/******************************************************************************
*  Copyright(c) 2021-2023 Sietium
*
*  @file:    terrain.cpp
*  @brif:    terrain
*
*  @date:    05/10/2021
*  @Author:  cary.Zhang
*
******************************************************************************/
#include <string>
#include <cstdlib>
#include <model.h>
#include <vector>
#include <array>
#include <memory>
#include "vmath.h"
#include "glad/gl-headers.h"

#include "glslprogram.h"
#include "texture.h"
#include "objectfactory.h"
#include "window.h"
#include "node.h"

static GLfloat vertices[] = {
    0.25f,  1.0f, 0.0f, 0.0f, 1.0f,
    0.25f, 0.25f, 0.0f, 0.0f, 0.0f,
    1.0f,  1.0f, 0.0f, 1.0f, 1.0f,

    0.25f, 0.25f, 0.0f, 0.0f, 0.0f,
    1.0f,  1.0f, 0.0f, 1.0f, 1.0f,
    1.0f, 0.25f, 0.0f, 1.0f, 0.0f,
};

class Terrain: public Node
{
public:
    Terrain();
protected:
    bool startup();
    void render(double currentTime, double difTime);
    void shutdown();
    void makeFrameBuffer();
    void makeRightTopVao();
    void makePbo();
private:
    GLSLProgram program;
    GLuint vao;
    GLuint texDisplacement;
    GLuint texColor;

    GLuint fbo;
    GLuint frameTexture;
    GLuint frameRbo;

    GLuint rectVao;
    GLuint rectVbo;

    GLSLProgram rectProgram;

    GLuint pbo;
    GLuint rightTopTex;
};

Terrain::Terrain()
{
    nodeName = "Terrain";
}

bool Terrain::startup()
{
    //glDrawArraysInstanced added in 3.1
    //glPatchParameteri added in 4.0
    //Binding points added in 4.2 or ARB_shading_language_420pack
    if (PerfWindow::glVersion < 420 &&
        (PerfWindow::glVersion < 400 ||
         !GLAD_GL_ARB_shading_language_420pack)) {
        printf("Failed! The GL version is less than 4.2 !\n");
        return false;
    }

    if (PerfWindow::glVersion > 400) {
        if (!program.build("../media/shaders/terrain/420/terrain.vs.glsl",
                        "../media/shaders/terrain/420/terrain.fs.glsl",
                        nullptr,
                        "../media/shaders/terrain/420/terrain.tcs.glsl",
                        "../media/shaders/terrain/420/terrain.tes.glsl"))
            return false;

        if (!rectProgram.build("../media/shaders/terrain/420/rect.vs.glsl", \
                            "../media/shaders/terrain/420/rect.fs.glsl")) {

            return false;
        }
    }
    else {
        if (!program.build("../media/shaders/terrain/400/terrain.vs.glsl",
                        "../media/shaders/terrain/400/terrain.fs.glsl",
                        nullptr,
                        "../media/shaders/terrain/400/terrain.tcs.glsl",
                        "../media/shaders/terrain/400/terrain.tes.glsl"))
            return false;

        if (!rectProgram.build("../media/shaders/terrain/400/rect.vs.glsl", \
                            "../media/shaders/terrain/400/rect.fs.glsl")) {

            return false;
        }
    }
    makeRightTopVao();
    makePbo();
    makeFrameBuffer();
    glGenTextures(1, &rightTopTex);

    glEnable(GL_DEPTH_TEST);

    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glPatchParameteri(GL_PATCH_VERTICES, 4);

    glActiveTexture(GL_TEXTURE0);
    texDisplacement = Texture::loadKtxImage("../media/textures/terragen1.ktx");

    glActiveTexture(GL_TEXTURE1);
    texColor = Texture::loadKtxImage("../media/textures/terragen_color.ktx");

    return 1;
}

void Terrain::render(double currentTime, double difTime)
{
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texDisplacement);
    glEnable(GL_CULL_FACE);

    (void)difTime;
    const GLfloat black[] = { 1.0f, 0.5f, 1.0f, 1.0f };
    const GLfloat one = 1.0f;

    float tf = static_cast<GLfloat>(currentTime * 0.03);
    float r = sinf(tf * 5.37f) * 15.0f + 16.0f;
    float h = cosf(tf * 4.79f) * 2.0f + 3.2f;

    glClearBufferfv(GL_COLOR, 0, black);
    glClearBufferfv(GL_DEPTH, 0, &one);

    glViewport(0, 0, getWidth(), getHeight());

    auto eye = vmath::vec3(sin(tf) * r, 4.8f, cos(tf) * h);
    vmath::mat4 mvMatrix = vmath::lookat(eye, vmath::vec3(0.0f), vmath::vec3(0.0f, 1.0f, 0.0f));
    vmath::mat4 projMatrix = vmath::perspective(45.0f,
                                                static_cast<GLfloat>(getWidth()) / static_cast<GLfloat>(getHeight()), 0.1f,
                                                1000.0f);

    program.use();
    glBindVertexArray(vao);

    program.setUniformMatrix4fv("mv_matrix", mvMatrix.operator float * ());
    program.setUniformMatrix4fv("mvp_matrix", (projMatrix * mvMatrix).operator float * ());
    program.setUniformMatrix4fv("proj_matrix", projMatrix.operator float * ());
    program.setUniform1f("dmap_depth", 6.0f);
    program.setUniform1i("enable_fog", 1);
    //program.setUniform4f("fog_color", 0.7, 0.8, 0.9, 0.0);

    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);

#ifndef USE_GLES
    /* GL_FILL is an inherent mode for GLES to draw polygons. No need to set GL_FILL.*/
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    glDrawArraysInstanced(GL_PATCHES, 0, 4, 64 * 64);

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
    glEnable(GL_CULL_FACE);
    glClearBufferfv(GL_COLOR, 0, black);
    glClearBufferfv(GL_DEPTH, 0, &one);
    glViewport(0, 0, getWidth(), getHeight());
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LEQUAL);
#ifndef USE_GLES
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    vmath::vec3 viewPos = vmath::vec3(eye[0], 10.0f, eye[2]);
    vmath::vec3 viewUp = vmath::vec3(0.0f, 0.0f, -1.0f);
    vmath::vec3 viewFront = viewPos + vmath::vec3(0.0f, -1.0f, 0.0f);
    mvMatrix = vmath::lookat(viewPos, viewFront, viewUp);
    program.setUniformMatrix4fv("mv_matrix", mvMatrix.operator float * ());
    program.setUniformMatrix4fv("mvp_matrix", (projMatrix * mvMatrix).operator float * ());
    glDrawArraysInstanced(GL_PATCHES, 0, 4, 64 * 64);

    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    glReadPixels(0, 0, getWidth(), getHeight(), GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
    glBindTexture(GL_TEXTURE_2D, rightTopTex);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, getWidth(), getHeight(), 0, GL_RGBA, GL_UNSIGNED_BYTE,
                 nullptr);
    glGenerateMipmap(GL_TEXTURE_2D);
    glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    glActiveTexture(GL_TEXTURE0);

    glDisable(GL_CULL_FACE);
    glDisable(GL_DEPTH_TEST);

    rectProgram.use();
    glBindVertexArray(rectVao);
    glDrawArrays(GL_TRIANGLES, 0, 6);
}

void Terrain::shutdown()
{
    program.clear();
    rectProgram.clear();

    glDeleteVertexArrays(1, &vao);

    glDisable(GL_CULL_FACE);
    glActiveTexture(GL_TEXTURE0);

    glDisable(GL_DEPTH_TEST);
#ifndef USE_GLES
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
#endif

    glDeleteTextures(1, &texDisplacement);
    glDeleteTextures(1, &texColor);

    glDeleteRenderbuffers(1, &frameRbo);
    glDeleteTextures(1, &frameTexture);
    glDeleteFramebuffers(1, &fbo);

    glDeleteVertexArrays(1, &rectVao);
    glDeleteBuffers(1, &rectVbo);

    glDeleteBuffers(1, &pbo);
    glDeleteTextures(1, &rightTopTex);

    glUseProgram(0);
}

void Terrain::makeFrameBuffer()
{
    glGenFramebuffers(1, &fbo);

    glBindFramebuffer(GL_FRAMEBUFFER, fbo);

    glGenTextures(1, &frameTexture);
    glBindTexture(GL_TEXTURE_2D, frameTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, getWidth(), getHeight(), 0, GL_RGBA, GL_UNSIGNED_BYTE,
                 nullptr);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, frameTexture, 0);
    glBindTexture(GL_TEXTURE_2D, 0);

    glGenRenderbuffers(1, &frameRbo);
    glBindRenderbuffer(GL_RENDERBUFFER, frameRbo);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, getWidth(), getHeight());
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, frameRbo);
    glBindRenderbuffer(GL_RENDERBUFFER, 0);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

void Terrain::makeRightTopVao()
{
    glGenVertexArrays(1, &rectVao);
    glBindVertexArray(rectVao);
    glGenBuffers(1, &rectVbo);
    glBindBuffer(GL_ARRAY_BUFFER, rectVbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), nullptr);
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat),
                          reinterpret_cast<void *>(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);

    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
}

void Terrain::makePbo()
{
    glGenBuffers(1, &pbo);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    glBufferData(GL_PIXEL_PACK_BUFFER,
                 static_cast<GLuint>(getWidth() * getHeight()) * sizeof(GLbyte) * 4, nullptr, GL_DYNAMIC_COPY);
    glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

REGISTER_OBJECT(Node, Terrain)
